-
Notifications
You must be signed in to change notification settings - Fork 232
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Automatic Gain Control #621
Add Automatic Gain Control #621
Conversation
Since I do not feel fully qualified to review the algorithmic side of this, I am going to ask the wider rodio community for help in reviewing. I will also read up on AGC and see if I can find any problem with the implementation.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very readable thanks! I have some minor things here and there. The algorithm makes sense to me though ill need to read up on AGC and look closer tomorrow. Did you come up with it yourself or did you find it somewhere? In either case we should attribute it in a comment somewhere.
Regarding the to_32
. That seems like a fine solution to me. I am wondering though would the algorithm still work if you had only i16? We could implement subtract, add multiply_by_f32, div_by_usize and modulo for Sample. There are trait in the std for all those I think.
That might save some conversions to f32, and in doing so speed up the implementation.
Ill look into it, but probably would need other people's input on what would be best to do there. It could be a premature optimization, but if it works, then that's a bonus. Thanks for the suggestion :)
I put this together myself, but I definitely used some algorithms and ideas I found online. So while I wrote the code, I built on what others have done before. By algorithms i mean mathematical ones
Where? |
- Implement asymmetric attack/release - Introduce MIN_ATTACK_TIME limit to prevent AGC instability - Clamp attack_time to prevent instability - Faster decrease, slower increase for smoother sound - Safeguard against extreme gain fluctuations
Integrated it with my GUI earlier, and it's working really well—Tested on a -10db audio file. Here’s a video showing the results: (Left is using sink without AGC) Video: 2024-09-27.20-00-04.mp4Let me know what you think of the new comment style and whether it is okay and descriptive enough. |
you got a good point there. It might very well be, its a real bummer rodio has no benchmarks/profile runs setup. We should not optimize without benchmark or profile. |
maybe the top of the source file? If you can list your inspiration and put your name on it, you made an algorithm for AGC you deserve some credit for that. Something along the likes of:
Its entirely optional if you do not feel like it you can leave this out. |
Great to hear! unfortunately the video is not playing for me (neither in browser or downloaded in VLC). Edit: video now works 🎉 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Honestly, this is the most well documented code I have seen in a long while ❤️. I am gonna show this to my friends. You have set a high bar for all the other PR's now :)
The only thing that could be improved is the --debug-gain
flag, I think checking an argument as a library is sup-optimal.
Your comment there mentions its for both debugging and monitoring. Do you mean its useful for the end user to be able to see the number? For example so they can tweak the parameters better?
If that's true then maybe a callback would be a good idea. Automatic gain control could get an extra argument (maybe an option?).
It would look like this then:
fn automatic_gain_control(
self,
target_level: f32,
attack_time: f32,
absolute_max_gain: f32,
gain_monitor: Option<Box<dyn Fn(f32)>>,
) -> AutomaticGainControl<Self>
The current monitoring code in fn next
could then become:
if let Some(callback) = self.gain_monitor {
callback(self.current_gain);
}
I have used Box<dyn Fn>
to prevent AGC getting a generic argument. That will incur a performance cost when using monitoring, its a tradeoff. What we could do is add a second source: MonitoredAutomaticGainControl
which is generic over the monitor callback. Then implement the current AutomaticGainControl
by just calling MonitoredAutomaticGainControl
with as callback a function that does nothing. The optimizer will optimize that function out for us.
Regarding logging for debugging purposes. Its probably a good idea to add either tracing or log to Rodio. We can enable them using a compile time feature so they are zero cost for any user that does not need them. They also integrate nicely in whatever application someone is writing.
I should really add some benchmarking to Rodio. AGC is now running for every sample, might be an idea to make it run only once every n samples to lower the perf hit of AGC. But that is definitely not for now but for a future PR/issue once we have benchmarking integrated into Rodio's CI. |
Running every n samples could work, but we risk issues with the audio not being able to adjust when needed. For example, a bass drop or a loud kick would possibly have a gain that is too high, making a crackling sound. Instead, we can make a threshold that logs the previous values and detects a large enough change. This is my testing while developing (AGC responsiveness threshold)
Although it most of the time indicates a 2.8% cpu usage, it's still such a small difference that I don't know if it's worth it. Plus, this is all using one core. By that, I mean 100% equals one core, and 200% equals two cores fully maxed out. In this case, we are using 1/36th of a core on average. So, maybe it's not worth it; the more delay we have, the more potential audio issues we will encounter, as it might not be able to react in time to lower it enough reasonably. In any case here is the code for that if we want to explore it in the future /// Checks if the peak level change is significant enough to trigger a gain update
///
/// This method calculates the absolute difference between the current peak level
/// and the last significant peak level. It considers a change to be significant
/// if this difference is 0.01 or greater, which is equivalent to a 1% change
/// in the signal level.
#[inline]
fn is_significant_change(&self) -> bool {
// Calculate the absolute difference directly using f32
let difference = (self.peak_level - self.last_significant_peak).abs();
// Consider a change significant if it's 0.01 or more (1% change)
difference >= 0.01
} |
definitely a good idea
Thank you so much! That really means a lot to me
Originally, it was just so I could view the changes while developing. However, now that I've refined the algorithm, it's become quite handy to read the values visually. This way, you don't have to guess using your ears. Realizing this, I'm planning to add a toggle for it, so we don't need to parse and loop over all arguments.
I don’t mind. If you're asking about the original inspiration for why I decided to work on this, it's because I am creating a graphical music player entirely in Rust, with the exception of two C backup dependencies. I wanted the audio to be automatically leveled and clear. |
e6c264e
to
97636d1
Compare
Force pushed so i don't add redundant commits to the history as i just merged the commits together |
This is a good idea but i wonder since our users will only do this once probably in debug mode why not just do // Output current gain value for debugging purposes
#[cfg(debug_assertions)]
println!("Current gain: {}", self.current_gain); The upside is that Rust's compiler's dead code elimination typically prevents unused functions and types from being included in the final binary of the application using this library. So, if people don't call |
I completely agree, thats not worth the effort and increased code complexity 👍 |
If anyone needs access to the value via a callback they can always open an issue, and then its easy enough to add. Always printing in debug mode seems like a bad idea, imagine having a tui app and in debug mode the view gets corrupted 😅. But the idea of determining compile time whether to print it is good. I see two options there (maybe there are more):
It seems best to go for the second option, though that is out of the scope for this PR, ill see if I can quickly add tracing to rodio. |
Yea that seems best also look into env_logger it's integrated directly with log and allows control over logging through environment variables Like this: Clarification I've never used any of these crates before, but I've heard of env_logger and the others. |
tracing builds upon log and env_logger, its a bit heavier to compile but I think that is okay since you have to opt into it. These crates are always split into two parts: the core that only gives you the I have added tracing on main now, see 95a466e on how to use it. And to see those traces in your tests/application you will want to do something like this: https://github.com/tokio-rs/tracing/blob/master/examples/examples/fmt.rs. For logging in AGC I would suggest |
Should be all done and ready to merge. This is my second ever PR and my first ever code PR, so I'm happy it's gone well. 🙂 Btw, if you're working on remaking the sink API, I don't mind helping if you'd have me |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would do the cfg experimental differently, and I think we still need an enable/disable function for the non atomicbool approach is missing. the rest looks perfect
and its a big one 600+ lines
whats needed most is ideas for API design. I have not used rodio a ton myself (ironic isnt it). So I am not familiar with all usecases. |
Don't know if you're saying that's a good or bad thing
I use it quite a lot in my music player GUI, in fact I've built around it |
well it means it was difficult/a lot of work most ppl start with a tiny PR :), you went big. So thats why it took a while. |
Well I learned a lot from it 😃 Waiting for another review when you have time (no pressure) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
otherwise ready to merge
hype 👍, loving having this in gonna be great for my podcast app, once I write it :) |
Yay, its finally merged 😌 |
Implemented automatic gain control (AGC)
I implemented a to_f32 conversion for the sample as it was missing. I'm not entirely certain if this is optimal, so I welcome feedback on the implementation.
This pull request is in relation to. Closes #620
Here is my
main.rs
andCargo.toml
i used to test while developing:Id suggest an attack_time of around 0.5 to 2.5 that seems to be a sweet spot for songs I tested.
Also, target_level I adjusted to be 0.9 just to make sure it does not clip the audio; it was clipping for me at 1.0.Don't know where to write documentation or if I should be the one writing it. Idk, let me know; thanks :)